#include <stdio.h>

void func1(int[]);

void func2(int[]);

void func3(int, int);

/**
 * 指针:
 * 1、我们一般把内存中的一个"字节"称为一个"内存单元"
 * 2、为了正确地访问这些内存单元，必须为每个内存单元编上号。根据一个内存单元的编号即可准确地找到该内存单元。
 * 内存单元的编号也叫做"地址"，而通常也我们将这个地址称之为"指针"。
 * 3、如果在程序中定义了一个变量，在对程序进行编译或运行时，系统就会给这个变量分配内存单元，并确定它的内存地址(编号)。
 * 4、变量的地址就是变量的指针，存放变量地址的变量则是"指针变量"。
 * 5、内存单元的"指针"和内存单元的"内容"是两个不同的概念。可以用一个通俗的例子来说明它们之间的关系。
 * 我们到银行去存取款时，银行工作人员将根据我们的帐号去找我们的存款单，找到之后在存单上写入存款、取款的金额。
 * 在这里，帐号就是存款单的指针，存款数是存款单的内容。对于一个内存单元来说，单元的地址即为指针，其中存放的数据才是该单元的内容。
 */
int main() {
    //指针变量的定义和使用
    //语法: 数据类型 *指针变量名;
    int *p1;//定义了一个指针变量p，简称指针p，p是变量，int *是类型(记住整体是类型，p1才是变量名)。——我们也认为指针是一种数据类型
    char *p2;
    //指针变量的赋值:
    //指针变量的值代表这个指针指向了以这个值为首地址的那块内存空间
    //TIPS:跟普通变量类似，指针变量在定义后如果不进行初始化，会指向任意一块内存地址，所以值是不确定的。%p则是输出地址值。
    printf("当前定义的指针指向的地址(未初始化):%p %p\n", p1, p2);
    int a = 10;
    //TIPS: &——取地址运算符。以前也经常使用过，但是从来没能进行深入介绍，其实该运算符的作用就是获取变量的地址。
    p1 = &a;
    printf("当前已成功赋值的指针指向的地址:%p\n", p1);
    //当然，指针也是变量的一种，所以这个变量也是存储在某块内存空间当中，同样可以使用&运算符获取
    printf("存储着该指针变量的地址的首地址:%p\n", &p1);
    //指针变量操作指向的内存地址——可以通过指针变量访问和修改所指向的内存空间中的内容。
    // *——指针运算符（或称"间接访问"运算符）。
    printf("通过指针访问当前a变量的值:%d\n", *p1);
    *p1 = 100;
    printf("通过指针修改了a变量的值:%d\n", a);

    //野指针——一个未初始化的指针变量的值是不确定的，我们称之为"野指针"。
    // 危害:由于其指向的空间是不确定的，所以如果直接操作很可能操作到非法的内存空间，导致程序运行崩溃。
    //如果该p2的值恰好是内存上一块只读的内存空间，则这句代码可以瞬间导致程序的崩溃，当然如果是可修改的则不会有问题。
    // 顺带一提，修改成功比报错的危害更大(因为你没有把握它到底修改了什么)，由于比较危险所以妥善测试。
//    *p2 = 'p1';

    //空指针。如果希望标志指针变量没有指向任何变量(空闲可用)，C语言中可以赋值一个NULL给此指针，这样就标志此指针为空指针。
    //本质: NULL是一个定义在stdio.h头文件的宏。#define NULL ((void *)0)。
    p2 = NULL;
    printf("当前指针变量p2指向的地址:%p\n", p2);
    //TIPS:空指针的作用是防止指针变量变成野指针。如果用*访问空指针所指向的内存空间也会导致程序报错(因为属于只读区)。
    //这个保证可以报错所以要测试的话可以打开。
//    printf("%c", *p2);

    //假设如果有需求需要程序员去访问某特定的内存位置并修改其中的值，比如说绝对地址000000000061FE0C中的值改为 0xaa66。
    //TIPS:此处只是为了演示，就不用绝对地址了，届时如果真有这种需求换成地址值即可——其实一般而言实际工作基本不可能遇到。
    printf("变量a的地址:%p\n", &a);
    int *point = (int *) (&a);
    *point = 0xaa66;
    printf("修改后变量a的值:%d\n", a);
    //当然在这里这个指针变量只是临时使用而已，完全可以不用定义，那么完全可以简写成：
    *(int *) (&a) = 0xaa55;
    printf("修改后变量a的值:%d\n", a);

    //不同类型指针变量的区别:
    //相同点:1、都是指针变量 2、都用来保存一个内存地址编号 3、占用的内存大小一样
    printf("指针占用的内存地址:%d %d\n", sizeof(p1), sizeof(p2));
    printf("指针占用的内存地址(再展示):%d %d\n", sizeof(double *), sizeof(float *));
    //输出结果如果你是32位机则是4，64位机则是8。为什么值一样且跟机器有关呢？
    //因为指针变量保存的是一个内存地址的编号!就这么简单。
    //32位机器内存地址编号最大值为2^32-1，用一个4字节的变量保存即可
    //64位机器内存地址编号最大值为2^64-1，则需用到一个8字节的变量保存了
    //不同点:
    // 首先你得明白一个前提，内存中所存储的仅仅只是二进制而已。而int char这样的数据类型只是程序员定义的希望将内存中的二进制当作某种数据类型来处理罢了。
    // 因此它们的不同之处就是int*指针会将内存中的二进制当作int类型处理，而char*指针会当作char类型处理，仅仅而已。
    //举例:将一个char *类型强行转换成int *类型
    char ch = 'A';
    p1 = (int *) &ch;
    //根据ASCII码，原ch变量值A应该是65，但是此处的值却并不是，这是因为虽然我们将ch的首地址赋予了int *，
    // 但是int *在处理时依旧会拿4个字节来处理!——而除了低地址处存储着1个字节的65之外，剩下3个字节存着什么就没法确定了。
    printf("将char *强转成int *的结果:%d\n", *p1);
    //当然，会出现异常数的情况是因为访问了4个字节，那么如果在使用时只访问一个字节就不会有问题了。
    printf("将char *强转成int *但是依旧只使用一个字节访问:%c\n", *p1);

    //运算符对指针的作用:
    printf("自增前当前定义指针指向的地址:%p %p\n", p1, p2);
    p1++;
    p2++;
    //从结果可以看到，指针变量+n，不是指针往后偏移n个字节，而是指针变量往后偏移n个数据类型。
    // 例如:p1+3，表示指针p1往后偏移3个int类型的数据，指针变量p1的新值=旧值+12(3*sizeof(int))
    // TIPS:指针移动后，意味着其指向了新的内存空间，则就偏离了原本指向的变量地址了，此时操作指针就不会影响到原指向的变量。
    printf("自增后当前定义指针指向的地址:%p %p\n", p1, p2);

    //指针与数组:
    //1、一个变量有一个地址，一个数据包含若干元素，每个数据元素都在内存中占用存储单元，它们都有相应的地址。
    // 因此所谓"数组的指针"指的是"数组的起始地址"。
    //2、数组名表示"数组的首地址"，因此数组名也是一种指针。
    //3、通过数组名访问数组中元素。
    int arr[] = {1, 2, 3, 4};
    printf("普通方式访问数组元素:%d\n", arr[3]);
    printf("指针引用方式访问数组元素(首地址):%d\n", *arr);
    //语法: *(arr + n)。TIPS: n <= sizeof(arr) / sizeof(arr[0])-1
    printf("指针引用方式访问数组元素:%d\n", *(arr + 2));
    printf("展示数组的首地址:%p %p\n", arr, &arr[0]);

    //用一个指针指向一个数组。因为arr是数组的首地址，所以该赋值代表指针point指向了arr这个数组的首地址。
    point = arr;
    printf("指向数组首地址的指针(存储该指针的首地址):%p\n", &point);
    printf("指向数组首地址的指针(它指向的数组首地址):%p\n", point);
    //而这种写法则代表这是一个"数组指针"->指向数组的指针。注意的是它指向的不是数组的首地址，而是"数组的引用"这么一个概念。
    int (*p)[] = &arr; //即便写成不带&编译器也会因为语法优化而自己加上。
    //但是这么定义是有问题的，会导致没法直接对变量使用 +n 或者[n] 这样的语法
    // 语法分析异常: Arithmetic on a pointer to an incomplete type 'int []'
//    printf("数组指针通过引用方式访问数组的地址: %d\n", *(p + 1));
    int (*ap)[4] = &arr; //因此还要加上数字才可以，等价于这是一个指向int[4]数组的指针。这样写应该更容易理解(虽然不能这么定义): int[4] (*ap)
    printf("数组指针通过引用方式访问数组的地址: %p\n", *(ap + 1));//注意，这里访问的是地址。因为ap=&arr,所以实际是 *(&arr + 1)
    printf("数组指针通过引用方式访问数组的元素: %d\n", *(*ap + 1));//所以需要再加一个*。因为ap=&arr,所以实际是 *(*(&arr) + 1)
    printf("数组指针自己的地址: %p\n", &p);
    printf("数组指针指向的数组本身的地址(引用地址): %p\n", p);
    printf("数组指针指向的数组指向的首地址: %p\n", *p);
    printf("数组指针指向的数组指向的首地址: %p\n", **p);
    // TIPS:数组由于其特殊性，导致了数组的引用与数组名本身的地址值是相同的。
    //  因此，对于大部分编译器而言，它们都是直接使用数组首地址作为常量替换掉数组名的。而这就是为什么数组变量初始化后没法进行再次初始化的原因。

    char testArr[40] = "直接初始化";
    // 语法分析异常: Array type 'char [40]' is not assignable
//    testArr = "再次初始化失败";

    //指针数组:顾名思义，就是存放指针的数组。本质是数组，只是数组中的每个元素都是指针。
    int *pArr[3];//等价于(int *)pArr[3] 因为[]优先级比*优先级高，所以会先于pArr匹配。
    int b = 30, c = 20, d = 10;
    //通过赋值初始化
    pArr[0] = &b;
    pArr[1] = &c;
    pArr[2] = &d;
    //指针变量的地址:还是要清晰认知一个概念——变量本身的地址和地址内保存的值是两个不同的概念。
    //所以pArr输出的就是存储这个pArr变量的内存空间的首地址，而*pArr输出的是该变量的内存空间中所存储的另一个指针变量&b的地址，
    // 所以**pArr所输出的是pArr变量的首地址所存储的指针变量&b的地址所存储的b变量的值，也就是30。
    printf("指针数组的首地址: %p %p\n", pArr, &pArr);
    printf("指针数组的首地址所指向的地址: %p\n", *pArr);
    printf("指针数组的首地址所指向的地址存储的值: %d\n", **pArr);

    //指针变量跟普通数组一样，可以定义时直接初始化
    //定义一个数组存储3个名称——存储的是字符串常量的首地址(注意不是字符串常量本身而是常量的首地址)
    //TIPS:字符串的本质就是一个固定字符长度的字符数组
    char *name[3] = {"zhangsan", "lisi", "wangwu"};
    printf("字符串占用的内存空间:%d个字节\n", sizeof("zhangsan"));//一共有8个字符，加上一个NUT空字符，所以共占用9个字节。
    printf("存储字符串的数组本质存储的是该字符串的首地址(也就是字符串的第一个字符的地址):%c\n", **name);

    int array[3] = {1, 2, 3};
    func1(array);
    printf("数组的首地址:%p\n", array);
    func2(array);

    //二级指针:用一个指针变量保存一个一级变量的地址，这个指针我们就称之为"二级指针"。
    // 语法: 数据类型 **变量名
    a = 10;
    point = &a;//point为一级指针，保存变量a的地址
    int **pp = &point;//pp为二级指针，保存一级指针point的地址
    //衍生概念:三级指针
    int ***p3 = &pp;
    printf("各级指针存储:%p %p %p %p\n", &a, point, *pp, **p3);
    printf("各级指针访问值:%d %d %d %d\n", a, *point, **pp, ***p3);


    //函数指针——本质：指针。作用：用来指向一个函数。
    //typedef关键字意为定义一个新的数据类型
    typedef void (*FUNC)(int, int);//定义一个函数指针类型，类型名称为FUNC(函数指针类型)，该函数指针类型的变量可以指向这么一类函数:返回值是void，形参为int,int

    //定义函数指针变量。
    //不使用typedef的场合
    void (*funcPoint)(int, int);
    //使用typedef的场合
    FUNC functionPointer;
    //将函数指针指向某个函数：函数的名字（函数的入口地址）。&可以加可以不加。
    funcPoint = &func3;
    functionPointer = func3;
    funcPoint(1, 2);
    functionPointer(1, 2);

    //指针函数——本质：函数。返回值是指针的函数(单纯的文字游戏)
    void *f();

    return 0;
}

//演示使用一级指针作为函数的形参。
// 当函数的形参为数组时，我们可以这样定义：
void func1(int a[]) {
    //为什么下方可以使用指针代替数组来定义呢？演示:
    //当形参为数组时，如何获取数组的长度呢？答案是做不到。
    //你会发现输出的结果是指针的长度，也就是说编译器在编译时将其当做了指针来处理了!
    // 编译警告——warning: 'sizeof' on array function parameter 'a' will return size of 'int *'
    printf("通过形参来获取数组的长度(不成功):%d\n", sizeof(a));
};

// 当然我们也可以将形参定义为指针类型(实际工作中常用!)
//调用函数时会将数组的首地址赋值给指针变量形参
void func2(int *a) {
    //TIPS:在实际的使用中，如果函数的形参为指针，一般都需要都指针的值进行判断，判断其是否为NULL
    if (NULL == a) return;
    printf("获取数组的首地址赋值给指针变量:%p\n", a);
    //依旧可以如同数组时一样通过下标访问元素(别忘了数组名本身也是一种指针)
    printf("通过存储的首地址访问数组元素:%d\n", a[0]);

};

void func3(int a, int b) {
    printf("函数指针成功调用了此函数\n");
}
